Djangoのクラスベースのビューの探訪 Part 2
著者:Leonardo Giordani - 11/12/2013 Updated on Mar 16, 2020
はじめに
この短期連載の第一回目では、Django のクラスベースのビューの理論と、その中でクラスが純粋な関数よりも強力である理由を紹介しました。また、Django がすぐに提供する汎用ビューの一つである ListView についても紹介しました。
2 回目の投稿では、2 番目によく使われるジェネリックビューである DetailView について、そしてカスタムクエリセットと引数についてお話したいと思います。最後に、より複雑な Web ページを作ることができる非特殊なクラスベースのビューを紹介します。ただし、DetailViewを完全に理解するためには、クエリーセットとビューパラメータという2つの重要な概念を把握する必要があります。というわけで、実践による学習の読者には申し訳ないが、今回も純粋なプログラミングの話題から始めようと思う。
クエリセット、または情報を抽出する技術
Django の最も重要な部分の 1 つは ORM (Object Relational Mapper) で、これを使うとデータベースを Python のオブジェクトの集合体のようにアクセスすることができます。ご存知のように、Django は DB クエリの構築を簡単にするツールを提供しています。それは、マネージャ (例えば、モデルの .objects 属性) とクエリメソッド (get や filter など) です。ここでは、一見しただけでは分からないほど複雑なので、注意してください。
マネージャのメソッドを使用すると、結果として QuerySet が得られます。これは通常、リストとして使用されますが、それだけではありません。クエリについてのドキュメントはこちら、クエリセットについてのドキュメントはこちらにあります。どちらもとてもお勧めの本です。
ここで強調しておきたいのは、スライスや反復処理など、コンテンツにアクセスするアクションを実行するまで、クエスチョンセットは評価されないということです。つまり、DBにアクセスすることなく、クエリセットを構築し、関数に渡し、保存し、さらにはプログラムやメタプログラミングで構築することができるのです。クエリセットをレシピのように考えれば、真実から遠くありません。クエリセットは、興味のあるデータを取得する方法を保存するオブジェクトです。実際にデータを取得することは、ゲームの別の部分です。このように、何かを定義することとその実行を分離することを遅延評価といいます。
なぜクエリセットの遅延評価が重要なのかを示すために、非常に些細な例を挙げてみましょう。
code: python
def get_oldest_three(queryset):
old_books = get_oldest_three(Book.objects.all())
old_hardcover_books = \
get_oldest_three(Book.objects.filter('type=Book.HARDCOVER'))
ご覧のとおり、get_oldest_three メソッドは入力された QuerySet(型は問わない)をフィルタリングしているだけで、オブジェクトを単純に並べ替え、最初の 3 つを DB に挿入します。ここで重要なのは、クエリセットを純粋なアルゴリズムやプロシージャの記述のように使っていることです。old_booksという変数を作ることで、get_oldest_threeメソッドに「おい、これは俺が興味のあるデータを抽出する方法だぞ」と伝えているのです。これを改良して実際のデータを返してくれないか?" と言っているのです。
このように柔軟なオブジェクトであるクエリセットは、ジェネリック・ビューの重要な部分ですので、次の宴会に備えて温めておきましょう。
柔軟性:パラメトリック・ビュー
URLはWebサイトやサービスのAPIです。このことは、ページを閲覧するユーザーにとっては多かれ少なかれ明らかなことですが、プログラマーの視点では、URLはWebベースのサービスのエントリーポイントです。静的なページは、定数や常に同じ値を返す関数(設定パラメータなど)のようなものであり、動的なページは、入力されたデータ(パラメータ)を処理して結果を返す関数のようなものだと言えます。
つまり、URLはパラメータを受け取ることができ、基礎となるビューも同じように受け取ることができるのです。HTTPを使ってブラウザからサーバーにパラメータを伝えるには、基本的に2つの方法があります。1つ目は、クエリストリング(Query string)と呼ばれる方法で、普遍的な構文を用いてURLに直接パラメータを列挙します。2つ目の方法は、HTTPリクエストのボディにパラメータを格納する方法で、POSTリクエストがこれにあたります。この方法については、後でフォームについての記事で説明します。 1つ目の方法には大きな欠点があります。ほとんどの場合、URLが長く(時には長すぎる)、実際のAPIとして使用するのが難しいのです。この問題を解決するために、Clean URL という概念が生まれ、Django はこの方法をネイティブに採用しています (ただし、必要に応じてクエリストリングにこだわることもできます)。 さて、Django の公式ドキュメントである URL ディスパッチャーでは、URL に含まれるパラメータを正規表現で解析して収集する方法が紹介されていますが、私たちが発見しなければならないのは、クラスベースのビューがどのようにパラメータを受け取って処理するかということです。
前回の記事では、クラスをインスタンス化してdispatchの結果を返す as_view メソッド(CODE)について説明しました。 code: python
def as_view(cls, **initkwargs):
"""
Main entry point for a request-response process
"""
# sanitize keyword arguments
for key in initkwargs:
if key in cls.http_method_names:
raise TypeError("You tried to pass in the %s method name as a "
"keyword argument to %s(). Don't do that."
% (key, cls.__name__))
if not hasattr(cls, key):
raise TypeError("%s() received an invalid keyword %r. as_view "
"only accepts arguments that are already "
"attributes of the class." % (cls.__name__, key))
def view(request, *args, **kwargs):
self = cls(**initkwargs)
if hasattr(self, 'get') and not hasattr(self, 'head'):
self.head = self.get
self.request = request
self.args = args
self.kwargs = kwargs
return self.dispatch(request, *args, **kwargs)
# take name and docstring from class
update_wrapper(view, cls, updated=())
# and possible attributes set by decorators
# like csrf_exempt from dispatch
update_wrapper(view, cls.dispatch, assigned=())
return view
ここで、viewラッパー関数がインスタンス化されたクラス(CODE)に対して実際に何をしているのかを見てみましょう。当然のことながら、URLconf から渡された request、 args、 kwargs を受け取り、同じ名前のクラス属性に変換しています。覚えておいてほしいのは、URLconf はこの関数自体を与えられているのであって、呼び出しの結果ではなく、ディスパッチの結果であるということです。 つまり、CBV のどこであっても、request、 args、 kwargsを読むだけで元の呼び出しパラメータにアクセスできるということです。ここで、*args と **kwargs は URLconf の正規表現で抽出された無名の値と名前付きの値です。
詳細の取得
物事のリストアップに続いて、Web サイトが行う最も便利なことの一つは、オブジェクトの詳細を提供することです。電子商取引のサイトは、ほとんどの場合、商品の一覧や商品の詳細を表示するページで構成されていますが、ブログも1つまたは複数のページで構成されており、記事の一覧や各記事のページがあります。ですから、私たちのデータベースのコンテンツの詳細なビューを構築することは、学ぶ価値があります。
この作業を手助けするために、Django は DetailView を提供しています。これは名前が示すように、確かに DB から取得した内容の詳細を扱うものです。ListView の基本動作は、与えられたモデルを持つ全てのオブジェクトのリストを抽出することですが、 DetailView は単一のオブジェクトを抽出します。どのようにして抽出されるべきオブジェクトを知るのでしょうか?
これは、HttpRequestオブジェクトの場合、使用されるHTTP動詞の名前(例:'GET')を含んでいます。dispatchは、動詞の小文字の名前を持つクラスのメソッドを探します(例:'GET'はgetになります)(CODE)。このハンドラは、ディスパッチと同じパラメータ、すなわち、リクエスト自体、*args、**kwargsで呼び出されます(CODE)。 DetailViewはボディを持たず、ListViewと同様に2つのクラスから全てを継承しています。最初の親クラスはテンプレートミキシンで、2番目のクラスであるBaseDetailViewはget`メソッドを実装しています(CODE)。 code: python
def get(self, request, *args, **kwargs):
self.object = self.get_object()
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
ご覧のように、このメソッドはget_objectを呼び出して表現する単一のオブジェクトを抽出し、次にget_context_data(前の記事ですでに出会っています)を呼び出し、最後におなじみのrender_to_responseを呼び出します。get_objectメソッドはBaseDetailViewの祖先であるSingleObjectMixin(CODE)によって提供されています:今回のトピックのために、そのコードの最も重要な部分は以下の通りです。 code: python
def get_object(self, queryset=None):
if queryset is None:
queryset = self.get_queryset()
pk = self.kwargs.get(self.pk_url_kwarg, None)
if pk is not None:
queryset = queryset.filter(pk=pk)
try:
obj = queryset.get()
return obj
警告 読みやすくするために、前の関数から多くの行を削除しました。完全な実装については、オリジナルのソースコードを確認してください。
get_querysetメソッドはSingleObjectMixin自身が提供しており、基本的にquerysetがあればそれを返し、なければ与えられたモデルのすべてのオブジェクトを返します(ListViewのように動作します)。このquerysetはfilterで絞り込まれ、最後にgetで絞り込まれます。ここでgetは直接使用されず、様々なエラーケースを管理し、正しい例外を発生させるために使用されます(だと思います)。
filter で使用されるパラメータ pk は kwargs から直接取得されるため、URL から直接取得されます。 これは一般的なビューの核となる概念なので、この部分は特に注意して見ていきたいと思います。
DetailViewクラスは、URLを解析するための正規表現を提供するURLconfによって呼び出されます。例えば、url(r'^(?P<pk>\d+)/$',. この正規表現はパラメータを抽出し、それにpkという名前をつけます。そのため、ビューのkwargsはpkをキーとし、URL中の実際の数字を値とします。例えば、URL 123/ の場合、{'pk': 123}。 DetailViewのデフォルトの動作は、pk_url_kwargが'pk'(CODE)であるため、pkキーを探し、それを使ってquerysetのフィルタリングを行います。
そのため、パラメータの名前を変更したい場合は、単にクラスのpk_url_kwargを定義し、新しい名前のプライマリキーを抽出する正規表現を提供することができます。例えば、url(r'^(?P<key>\d+)/$')はkeyという名前で抽出するので、クラスにpk_url_kwarg = 'key'を定義する必要があります。
以上のことから、DetailView.Baseを継承したクラスはオブジェクトキーを初期化したコンテキストを提供することがわかりました。
オブジェクトキーが1つのオブジェクトに初期化されたコンテキストを提供する。
どのオブジェクトを抽出するかを知るために、モデルクラス属性で設定する必要がある。
単一オブジェクトが抽出されるオブジェクトのセットを絞り込むために、querysetクラス属性で構成することができます。
検索されたオブジェクトのプライマリキーをpkとして抽出する正規表現を含むURLから呼び出す必要があります。
pk_url_kwargクラス属性により、プライマリキーに別の名前を使用するように設定できます。
DetailViewの基本的な使い方は、以下のコードで例示されています。
code: python
class BookDetail(DetailView):
model = Book
urlpatterns = patterns('',
url(r'^(?P<pk>\d+)/$',
BookDetail.as_view(),
name='detail'),
)
ビューは、Bookモデルを持つ単一のオブジェクトを抽出します。正規表現は、標準的なpk名で構成されています。
前の記事でListViewについて示したように、どのCBVもget_context_dataを使って、レンダリングエンジンにコンテキスト辞書を返します。ですから、DetailViewを継承したビューは、同じパターンでコンテキストにデータを追加することができます。例えば、get_similar_booksという関数があって、本が与えられると、ある基準に従って似たような本を返すとします。
code: python
class BookDetail(DetailView):
model = Book
def get_context_data(self, **kwargs):
context = super(BookDetail, self).get_context_data(**kwargs)
context'similar' = get_similar_books(self.object) return context
urlpatterns = patterns('',
url(r'^(?P<pk>\d+)/$',
BookDetail.as_view(),
name='detail'),
)
先に説明したように、表示されているオブジェクトにはobjectを通じてアクセスできます。上記の例では、コードのどこかに実装したサービス関数に渡されています。
ベースビューの利用
複雑なページを扱っていると、Django が提供する汎用の表示用 CBV が適切な選択でないことがあります。これは通常、ビューが標準的な動作をしないようにメソッドをオーバーライドし始めると明らかになります。例えば、複数のオブジェクトの詳細情報を表示したいとしましょう。おそらく DetailView は、1つのオブジェクトを表示するために作られているので、すぐに限界が来るでしょう。
一般的な表示CBVの1つでは簡単に解決できないすべてのケースでは、基本的なビューの1つから独自に構築する必要があります。RedirectView、TemplateView、View (DOCS, CODE)のいずれかの基本ビューから独自に構築する必要があります。 これらのビューについて完全に説明するつもりはありませんが、いくつかの特殊性を簡単に指摘したいと思います。
as_viewとdispatchメソッドについて説明したときに出会ったViewは、もう旧知の仲です。これは最も一般的なビュークラスで、テンプレートを使わずにページをレンダリングするなど、非常に特殊なタスクを実行するために活用することができます(例えば、JSONデータを返す場合など)。
TemplateViewは、テンプレートからページをレンダリングするのに最適なクラスで、コンテキストディクショナリの内容に関しては非常に高い自由度を維持しています。ListViewとDetailViewの後に最も使用するビューになる可能性があります。基本的には、これを継承して、get_context_dataメソッドを定義するだけです。CODE から分かるように、TemplateViewはGETリクエストにのみ応答します。 RedirectViewは、その名の通り、リクエストをリダイレクトするために使用されます。リダイレクトのメカニズムは非常にシンプルで、getメソッドはurlクラス属性で定義されたURLにHttpResponseRedirectを返します。このクラスは、GET以外のHTTPメソッド(HEAD、POST、OPTIONS、DELETE、PUT、PATCH)で呼び出されたときに、非常に興味深い動作(CODE)を示します:それぞれのメソッド(head、postなど)からgetを呼び出すだけで、メソッドをGETに「変換」します。次の記事では、このシンプルなテクニックを利用して、ユーザーに入力済みのフォームを表示する方法を紹介します。 日付ベースのビュー(Date-based views)
Django には他にもクラスベースのビューがあり、日付で抽出したり並べたりした オブジェクトを簡単に扱うことができます。YearArchiveView や DayArchiveView (CODE) のようなビューは、日付ベースのオブジェクトを扱いやすくすることを目的としています。日付を含むオブジェクト (例: 記事の投稿日、人の生年月日、メッセージのログの日付など) は、これらのビューで処理できます。公式ドキュメント も参照してください。 日付ベースのビューはCBVなので、ListViewやTemplateViewのようにViewをベースにしていることを覚えておいてください。ですから、日付処理に特化していることを除けば、同じように動作します(get_context_data、get、dispatchなどを使用)。
最後に
今回の記事では、DetailViewについて深く掘り下げて説明し、さらに表面的には残りのすべてのベースビューとデータベースビューについて説明しました。DetailViewが要求されたオブジェクトを見つけるために、与えられたモデルとクエリリングのパラメータをどのように使用するか、また、そのデフォルトの動作をどのように変更できるかを紹介しました。次の記事では、フォームの豊かな(そして奇妙な)世界に足を踏み入れます。
Digging up Django class-based views series
日本語訳:Djangoのクラスベースのビューの探訪 Part 2
日本語訳:Djangoのクラスベースのビューの探訪 Part 3